#!/usr/bin/env php
<?php
//$SOAR_DIR=getenv("SOAR_DIR");
//if(empty($SOAR_DIR)) $SOAR_DIR="/opt/soar";
$SOAR_DIR="../../../../..";
include "$SOAR_DIR/common/php/soar/soar.inc";

class share
{/*{{{*/
    public static $stat_lock;
    public static $stat;

    public static $operate_lock;
    public static $operate;
}/*}}}*/

function load_cnfg()
{/*{{{*/
    $cnfg_file=dirname(__FILE__)."/server.cnfg.inc";
    include $cnfg_file;

    if(empty(cnfg::$WORKERS))
    {
        printf("%s: WORKERS not set\n", __FUNCTION__);
        return false;
    }
    if(cnfg::$WORKERS < 1 || cnfg::$WORKERS > 32)
        cnfg::$WORKERS=4;

    if(empty(cnfg::$ACCESS['SERVER_PORT']) ||
            cnfg::$ACCESS['SERVER_PORT'] < 1024)
    {
        printf("%s: ACCESS.SERVER_PORT invalid\n", __FUNCTION__);
        return false;
    }
    if(empty(cnfg::$ACCESS['MONITOR_PORT']) ||
            cnfg::$ACCESS['MONITOR_PORT'] < 1024)
    {
        printf("%s: ACCESS.MONITOR_PORT invalid\n", __FUNCTION__);
        return false;
    }

    return true;
}/*}}}*/

class worker
{/*{{{*/
    private static $svc_call_count=array();

    private function proc($worker_idx, $new_sock)
    {/*{{{*/
        static $TIMEOUT=10;

        $end=false;
        while(!$end)
        {
            $timeout=$TIMEOUT;
            $res=sock::recv($new_sock, $request, $timeout);
            if($res == false)
            {
                errlog::add("%s: worker(%d) sock::recv fail",
                        __METHOD__, $worker_idx);
                break;
            }

            $respond="";
            $curr=0;
            $svc=para::get_item($request, $curr);
            if(empty($svc) || strlen($svc) > misc::NAME_SIZE)
            {
                errlog::add("%s: worker(%d) svc(%s) invalid",
                        __METHOD__, $worker_idx,
                        is_null($svc)?"null":$svc);
                para::put_item($respond, SOAR_ERR_SVC_INVALID); 
            }
            else
            {
                if($svc == "CLOSE")
                {
                    para::put_item($respond, SOAR_ERR_OK); 
                    $end=true;
                }
                else
                {
                    @include_once "./svc/${svc}.inc";
                    $func="SVC_FUNC_$svc";
                    if(function_exists($func))
                    {
                        $respond=$func($request);
                        if(array_key_exists($svc, self::$svc_call_count))
                            ++self::$svc_call_count[$svc];
                        else
                            self::$svc_call_count[$svc]=1;
                    }
                    else
                    {
                        errlog::add("%s: worker(%d) func(%s) not found",
                                __METHOD__, $worker_idx, $svc);
                        para::put_item($respond, SOAR_ERR_SVC_NOT_FOUND);
                    }
                }
            }

            $timeout=$TIMEOUT;
            $res=sock::send($new_sock, $respond, $timeout);
            if($res == false)
            {
                errlog("%s: worker(%d) sock::send fail",
                        __METHOD__, $worker_idx);
                break;
            }
        }
    }/*}}}*/

    private function loop($worker_idx, $listen_sock)
    {/*{{{*/
        static $TIMEOUT=2;
        static $UPDATE_STAT_INFO_INTERVAL=10;
        static $CHECK_OPERATE_INFO_INTERVAL=3;

        $last_stat=0;
        $last_operate=0;
        $client_conn_count=0;

        $quit=false;
        while(!$quit)
        {
            $timeout=$TIMEOUT;
            $new_sock=sock::accept($listen_sock, $timeout);
            if($new_sock === false)
            {
                if($timeout != 0)
                {
                    errlog::add("%s: worker(%d) sock accept fail",
                            __METHOD__, $worker_idx);
                }
            }
            else
            {
                //printf("worker(%d) new client:%d\n",
                //        $worker_idx, $new_sock);
                self::proc($worker_idx, $new_sock);
                socket_close($new_sock);
                ++$client_conn_count;
            }

            $now=time();

            if($now-$last_stat > $UPDATE_STAT_INFO_INTERVAL)
            {/*{{{*/
                if($client_conn_count > 0)
                {
                    $client_call_count=0;

                    sem_acquire(share::$stat_lock);
                    $stat=shm_get_var(share::$stat, 0);

                    foreach(self::$svc_call_count as $name=>$count)
                    {
                        if($count > 0)
                        {
                            if(array_key_exists($name, $stat['svc']))
                                $stat['svc'][$name]+=$count;
                            else
                                $stat['svc'][$name]=$count;
                            $client_call_count+=$count;
                        }
                    }
                    self::$svc_call_count=array();

                    $stat['worker'][$worker_idx]['client_conn_num']+=
                        $client_conn_count;
                    $stat['worker'][$worker_idx]['client_call_num']+=
                        $client_call_count;

                    shm_put_var(share::$stat, 0, $stat);
                    sem_release(share::$stat_lock);

                    $client_conn_count=0;
                }

                $last_stat=$now;
            }/*}}}*/

            if($now-$last_operate > $CHECK_OPERATE_INFO_INTERVAL)
            {/*{{{*/
                sem_acquire(share::$operate_lock);
                $operate=shm_get_var(share::$operate, 0);

                $quit=$operate['quit'];
                if($quit)
                {
                    $operate['worker'][$worker_idx]='S';
                    shm_put_var(share::$operate, 0, $operate);
                    errlog::add("%s: worker(%d) quit(%d)",
                            __METHOD__, $worker_idx, $quit);
                }

                sem_release(share::$operate_lock);

                $last_operate=$now;
            }/*}}}*/
        }
    }/*}}}*/

    public function start($listen_sock)
    {/*{{{*/
        for($i=0; $i<cnfg::$WORKERS; ++$i)
        {
            $pid=pcntl_fork();
            if($pid == -1)
            {
                printf("%s: fork workers fail\n", __FUNCTION__);
                return false;
            }
            if($pid == 0)
            {
                self::loop($i, $listen_sock);
                exit();
            }
        }

        return true;
    }/*}}}*/
}/*}}}*/

class monitor
{/*{{{*/
    private static $quit=false;
    private static $cmd_map=array(
            array(
                'name'=>"printworker", 'ab'=>"pwrk",
                'desc'=>"print client ip & port",
                'usage'=>"printclient(pwrk)"
                ),
            array(
                'name'=>"printservice", 'ab'=>"psvc",
                'desc'=>"print service status",
                'usage'=>"printservice(psvc)"
                ),
            array(
                'name'=>"shutdown", 'ab'=>"shutdown",
                'desc'=>"shutdown server",
                'usage'=>"shutdown"
                ),
            array(
                'name'=>"help", 'ab'=>"h",
                'desc'=>"",
                'usage'=>""
                )
                );

    private function CMD_FUNC_PRINTWORKER($request)
    {/*{{{*/
        $respond=
            "Worker         Connected        Calls\n".
            "------         ---------        -----\n";
        sem_acquire(share::$stat_lock);
        $stat=shm_get_var(share::$stat, 0);
        for($i=0; $i<cnfg::$WORKERS; ++$i)
        {
            $worker=$stat['worker'][$i];
            $respond.=sprintf("%6d         %9d    %9d\n",
                    $i,
                    $worker['client_conn_num'],
                    $worker['client_call_num']);
        }
        sem_release(share::$stat_lock);
        return $respond;
    }/*}}}*/

    private function CMD_FUNC_PRINTSERVICE($request)
    {/*{{{*/
        $respond=
            "Service                         Calls\n".
            "-------                         ---------\n";
        sem_acquire(share::$stat_lock);
        $stat=shm_get_var(share::$stat, 0);
        foreach($stat['svc'] as $name=>$num)
        {
            $respond.=sprintf("%-30s  %9d\n",
                    $name, $num);
        }
        sem_release(share::$stat_lock);
        return $respond;
    }/*}}}*/

    private function CMD_FUNC_SHUTDOWN($request)
    {/*{{{*/
        sem_acquire(share::$operate_lock);
        $operate=shm_get_var(share::$operate, 0);
        $operate['quit']=true;
        shm_put_var(share::$operate, 0, $operate);
        sem_release(share::$operate_lock);

        sleep(5);
        sem_acquire(share::$operate_lock);
        $operate=shm_get_var(share::$operate, 0);
        $finish=true;
        for($j=0; $j<cnfg::$WORKERS; ++$j)
        {
            $worker=$operate['worker'][$j];
            if($worker == 'R')
                $finish=false;
        }
        sem_release(share::$operate_lock);
        if($finish)
        {
            $respond="Shutdown complete\nDisconnect\n";
            self::$quit=true;
        }
        else
        {
            $respond="Shutdown not complete, try again\n";
        }
        return $respond;
    }/*}}}*/

    private function CMD_FUNC_HELP($request)
    {/*{{{*/
        if(empty($request))
        {
            $respond="help [command]\ncommands:\n";
            foreach(self::$cmd_map as $arr)
            {
                $respond.="    ".$arr['name']."\n";
            }
            return $respond;
        }

        $request=strtolower($request);
        foreach(self::$cmd_map as $arr)
        {
            if($arr['name'] == $request || $arr['ab'] == $request)
            {
                $respond="Description: ".$arr['desc']."\n".
                    "Usage: ".$arr['usage']."\n";
                return $respond;
            }
        }
        $respond="Command not found, type help\n";
        return $respond;
    }/*}}}*/

    private function cmd($request)
    {/*{{{*/
        $found=false;
        foreach(self::$cmd_map as $arr)
        {
            if($arr['name'] == $cmd || $arr['ab'] == $cmd)
            {
                $found=true;
                $cmd=$arr['name'];
                break;
            }
        }
        if($found)
        {
            $func="CMD_FUNC_".$cmd;
            $respond=self::$func($body);
        }
        else
        {
            $respond="command(".$cmd.") not found\n";
        }

        return $respond;
    }/*}}}*/

    public function start($monitor_sock)
    {/*{{{*/
        while(!self::$quit)
        {
            $console_sock=socket_accept($monitor_sock);
            if($console_sock === false) continue;

            $console_quit=false;
            while(!self::$quit && !$console_quit)
            {
                $res=socket_recv($console_sock, $request, 500, 0);
                if($res === false || $res == 0)
                {
                    errlog::add("%s: read console fail", __METHOD__);
                    $console_quit=true;
                    continue;
                }

                $request=trim($request);
                $pos=strpos($request, " ");
                if($pos === false)
                {
                    $cmd=$request;
                    $body="";
                }
                else
                {
                    $cmd=substr($request, 0, $pos);
                    $body=substr($request, $pos);
                    $body=ltrim($body);
                }
                $cmd=strtolower($cmd);
                //printf("cmd(%s)\n", $cmd);
                $cmdsize=strlen($cmd);
                if(0 < $cmdsize && $cmdsize <= misc::NAME_SIZE) 
                {
                    $found=false;
                    foreach(self::$cmd_map as $arr)
                    {
                        if($arr['name'] == $cmd || $arr['ab'] == $cmd)
                        {
                            $found=true;
                            $cmd=$arr['name'];
                            break;
                        }
                    }
                    if($found)
                    {
                        $func="CMD_FUNC_".$cmd;
                        $respond=self::$func($body);
                    }
                    else
                    {
                        $respond="command(".$cmd.") not found\n";
                    }
                }
                else
                {
                    $respond="command invalid\n";
                }

                $res=socket_send($console_sock,
                        $respond, strlen($respond), 0);
                if($res === false || $res == 0)
                {
                    errlog::add("%s: write console fail", __METHOD__);
                    $console_quit=true;
                    continue;
                }
            }
            socket_close($console_sock);
        }

        errlog::add("%s: shutdown", __METHOD__);
        return true;
    }/*}}}*/
}/*}}}*/

function start($daemon)
{/*{{{*/
    if($daemon)
    {
        $pid=pcntl_fork();
        if($pid == -1)
        {
            printf("%s: daemonize fail", __METHOD__);
            return false;
        }
        if($pid > 0)
        {
            return true;
        }
        // errlog
        if(errlog::init(basename(__FILE__), cnfg::$ERRLOG['LOCATION'])
                == false)
        {
            printf("%s: init errlog fail", __METHOD__);
            return false;
        }
    }

    // setuid
    if(posix_getuid() == 0)
    {
        if(!empty(cnfg::$USER))
        {
            $user=posix_getpwnam(cnfg::$USER);
            if($user)
            {
                if(posix_setuid($user['uid']) == false)
                {
                    printf("Change to user %s fail\n", cnfg::$USER);
                    return false;
                }
                printf("Change to user %s\n", cnfg::$USER);
            }
            else
            {
                printf("User %s not found, keep root\n", cnfg::$USER);
            }
        }
        else
        {
            printf("User empty, keep root\n");
        }
    }

    // 初始化共享信息
    $key=ftok(__FILE__, "s");
    if(($id=sem_get($key)) === false ||
            sem_remove($id) == false ||
            ($id=sem_get($key)) === false)
    {
        printf("%s: init sem(share) fail\n", __METHOD__);
        return false;
    }
    share::$stat_lock=$id;
    if(($id=shm_attach($key)) === false ||
            shm_remove($id) == false ||
            ($id=shm_attach($key)) === false)
    {
        printf("%s: init shm(share) fail\n", __METHOD__);
        return false;
    }
    for($i=0; $i<cnfg::$WORKERS; ++$i)
        $stat['worker'][$i]=
            array('client_conn_num'=>0, 'client_call_num'=>0);
    $stat['svc']=array();
    shm_put_var($id, 0, $stat);
    share::$stat=$id;

    $key=ftok(__FILE__, "o");
    if(($id=sem_get($key)) === false ||
            sem_remove($id) == false ||
            ($id=sem_get($key)) === false)
    {
        printf("%s: init sem(operate) fail\n", __METHOD__);
        return false;
    }
    share::$operate_lock=$id;
    if(($id=shm_attach($key)) === false ||
            shm_remove($id) == false ||
            ($id=shm_attach($key)) === false)
    {
        printf("%s: init shm(operate) fail\n", __METHOD__);
        return false;
    }
    for($i=0; $i<cnfg::$WORKERS; ++$i)
        $operate['worker'][$i]='R';
    $operate['quit']=false;
    shm_put_var($id, 0, $operate);
    share::$operate=$id;
    $operate=shm_get_var($id, 0);

    $monitor_sock=sock::listen(cnfg::$ACCESS['MONITOR_PORT']);
    if($monitor_sock === false)
    {
        errlog::add("%s: open monitor_port(%d) fail",
                __METHOD__, cnfg::$ACCESS['MONITOR_PORT']);
        return false;
    }
    $listen_sock=sock::listen(cnfg::$ACCESS['SERVER_PORT']);
    if($listen_sock === false)
    {
        printf("%s: open server_port(%d) fail",
                __METHOD__, cnfg::$ACCESS['SERVER_PORT']);
        return false;
    }

    if(worker::start($listen_sock) == false)
        return false;

    return monitor::start($monitor_sock);
}/*}}}*/

function main($argv)
{/*{{{*/
    $daemon=false;
    if(isset($argv[1]))
    {
        if($argv[1] == "-d" || $argv[1] == "--daemon")
           $daemon=true; 
    }
    if(load_cnfg() == false)
        return 1;
    if(start($daemon) == false)
        return 1;
    return 0;
}/*}}}*/

exit(main($argv));
?>
